#!/home/richard/bin/lua -i
--
-- (c) 2006 Richard Simes
-- You may distribute this program under the terms of the 
-- GNU General Public License. See COPYING for details.
--


-- should really configure this from Makefile
--package.path="./?.lua;./?.lc;/usr/share/luacalc/?.lua"


luacalc={
	-- functions for use in sheet cells
	functions = {},
	
	-- the actual sheet with values.
	sheet = {},
	
	-- Different views/masks
	html_view = {}, -- TODO
	display_view = {},
	csv_view = {},
	selections = {}, -- standard place to store selections
	inputmode="normal",  -- the input mode
	pastedata={},
	-- whether the file has been changed or not.
	dirty = false,
	-- input cell locaton
	x = 1,
	y = 1,
	-- macro
	macro_running = false,
	-- version number
	version="0.7.0"
}

require"stringaux"
require"lcstack"

-- import luacalc cell functions
require"lcfunctions"

-- plotting

require"plot"

-- Sheet or view iterator
-- unfortunately ipairs() doesn't work
-- for views as they don't contain any data.
-- and a get-and-check iterator won't work either
-- as we are using semi auto-magic/self expanding tables.
function luacalc.foreach(tab, func, eolfunc)
   for x=1, #luacalc.sheet do
      for y=1, #luacalc.sheet[x] do
	 func(tab[x][y])
      end
      if eolfunc then
	 eolfunc(x)
      end
   end
end


-- Sheet or view iterator.
-- alternate ordering to luacalc.foreach
function luacalc.forall(tab, func, eolfunc)
   for y = 1, #luacalc.sheet do
      for x = 1, #luacalc.sheet[1] do
	 func(tab[x][y])
      end
      if eolfunc then
	 eolfunc(x)
      end
   end

end

-- Convert a string such as A, AB... to an index.
-- doesn't do any error checking - use with caution
function luacalc.string_to_index(s)
   if string.len(s) == 1 then
      return string.byte(s, 1)-string.byte("A", 1) + 1
   else
      return luacalc.string_to_index(string.sub(s, 1, 1))*26 + luacalc.string_to_index(string.sub(s, 2))
   end
end

--WARING: only valid up to Z (until i need it higher...
function luacalc.index_to_string(i)
	local res = ""
	res = res..string.char(string.byte"A"+i-1)
	return res
end

-- returns the value s unless it is a function:
-- if s is a string, starting with '='
-- the string will be loaded as a lua function 
-- and run with the result returned.
function luacalc.raw_value(s, x, y)
   if type(s)=="string" and string.sub(s, 1, 1) == "=" then
      -- deal with ':' here. need to parse for text outside of quotes
      local form = luacalc.parse_formula(s, x, y)
      --print(s)
      --
      local f, err = loadstring("return "..string.sub(form, 2))
      if err then
	 luacalc.sheet[x][y]=("'"..s )
	 return ("'"..s)
      end
      luacalc.sheet[x][y] = {text=s, func=f}
      return f()
   elseif type(s)=="table" then
      return s.func()
   elseif type(s)=="boolean" then
      return IF(a, "true", "false")	
   else
      return s
   end
end

-- 
-- parse the command for the range operator, ':' and replace with a
-- call returning a table with all the values in the selection
-- also replaces $x and $y with the x, y cell reference

function luacalc.parse_formula(str, x, y)
	if string.len(str) == 0 then return str end
	-- find quotes
	local start, finish, quoted = string.find(str, '(".-")')

	if start and finish then
		-- parse parts before and after the quoted string
		local bef = string.sub(str, 1, start-1)
		local aft = string.sub(str, finish+1, string.len(str))
		return luacalc.parse_formula(bef, x, y)..quoted..luacalc.parse_formula(aft, x, y)
	end
	-- coordinate substitution
	str = string.gsub(str, "$x", x)
	str = string.gsub(str, "$y", y)
	
	-- deal with colons - no quoted strings should be here
	start, finish =string.find(str, "%u+%d+[:]%u+%d+")
	if start and finish then
	local colon = string.find(str, ":")
	local x1,y1 = luacalc.cellindex(string.sub(str, start, colon-1))
	local x2,y2 = luacalc.cellindex(string.sub(str, colon+1, finish))
	local res = string.sub(str, 1, start-1)
	res = res.."luacalc.getselection{"..x1..","..y1..","..x2..","..y2.."}"
	res = res..string.sub(str, finish+1, string.len(str))
	return res
	end
	-- String contains no colon, so just return it.
	return str
end

--
-- returns the (x, y) index of a cell given a string of the form A1
--
function luacalc.cellindex(str)

   local letter=""
   local number=""
   string.foreach(str, function(ind, char)
			  -- surely there is a more efficient check for capitals?
			  if string.byte(char) >= string.byte"A" and string.byte(char) <= string.byte"Z" then
			     letter=letter..char
			  else
			     number=number..char
			  end
		       end)
   local rowind = luacalc.string_to_index(letter)
   local ind = tonumber(number)
   return rowind, ind
end



function luacalc.getselection(tab)
   local x1 = tab.x1 or tab[1]
   local y1 = tab.y1 or tab[2]
   local x2 = tab.x2 or tab[3]
   local y2 = tab.y2 or tab[4]

   local   res = {}
   if x1 == x2 then
      for i=y1, y2 do
	 table.insert(res,luacalc.display_view[x1][i])
      end
   elseif y1==y2 then
      for i=x1, x2 do
	 table.insert(res,luacalc.display_view[i][y1])
      end      
   else
      for j = x1, x2 do
	 local inner = {}
	 for i=y1, y2 do
	    table.insert(inner,luacalc.display_view[j][i])
	 end
	 
	 table.insert(res, inner) 
      end
   end
   return res
end


function luacalc.select(tab)
   assert(#tab==4)
   return tab
end







-- Automagic tables.
-- If a non existant column is indexed with a number, add it
-- If we have indexed with a string, convert to a colum no where
-- A is 1, B is 2.... AA is 27

setmetatable(luacalc.sheet, {
		__index = function(table, key)
			     if type(key) == "number" then
				table[key] = {}
				return table[key]
				-- if key is an all uppercase string
			     elseif type(key) == "string" and string.find(key, "^%u+$") then
			     	-- return a whole column
				return table[luacalc.string_to_index(key)]	 
			     end
			  end
	     }
	  )
	     

-- ok....
-- here we set a metatable of a view, so it returns a table containing 
-- nothing but the key used to reference it. the table returned has a 
-- metatable, with the __index function as given in 'func'
function luacalc.metasetup(lctab, func)
   setmetatable(lctab,  {
		   __index = function(tab, key)
				local res = {index=key}
				setmetatable(res, {__index=func
					     })
				return res
			     end
		}
	     )
end


-- not really csv view anymore - serial view as well
luacalc.metasetup(luacalc.csv_view, 
		  function(t, k)
		     local val = luacalc.sheet[k][t.index]
		     if type(val)=="table" then
		     	val = val.text
		     end
		     
		     if type(val)=="string" then
		     	val = string.format("%q", val)
		     end
		     if val then
			val = val..","
		     end
			
		     
		     return val
		  end
	       )

-- save the sheet to the file given
-- would be good if string.concat could
-- be used, but we need to quote strings
-- and get the function text.
function luacalc.save_file(fname)

   local f = io.open(fname, "w")

   luacalc.forall(luacalc.csv_view, 
		   function(val)
		      if val then
		       f:write(val)
		      end
		   end,
		    function()
		       f:write("\n")
		    end
		)
   f:close()
	luacalc.dirty=false

end



-- 'normal' unquoted, empty valued csv files
function luacalc.load_file_csv(fname)
   y= 1
   local f = io.open(fname, 'r')
   for line in f:lines() do
      x=1
      for _, word in ipairs(luacalc.split(line, "[,]+")) do
	 if word then
	    luacalc.sheet[x][y] = tonumber(word) or word
	    x=x+1
	 end
      end
      y = y+1
   end
   f:close()
	luacalc.dirty=false
end

-- 'proper' csv files.
-- auto translated to lua, so each value must be a valid lua value
-- thanks to lhf for the help with this.
function luacalc.load_file(fname)
   local state=1
   local file = io.open(fname)
   local f = function()
		if state==1 then
		   state=2
		   return "luacalc._load{"
		end
		if state==2 then
		   local line = file:read()
		   if line then 
		      return "{"..line.."}," 
		   else
		      state=3
		      return "}"
		   end
		end
		if state==3 then 
		   return nil
		end
	     end
	luacalc.dirty=false
   return assert(load(f))()
end



--[[
--sets the table given as the current sheet
--]]
function luacalc._load(tab)
	luacalc.sheet=tab
	setmetatable(luacalc.sheet, {
		__index = function(table, key)
			     if type(key) == "number" then
				table[key] = {}
				return table[key]
			     end
			  end
	     }
	  )

end

-- display the calculated value
luacalc.metasetup(luacalc.display_view,
		   function (tab, k)
		      local val = luacalc.sheet[k][tab.index]
		      return luacalc.raw_value(val, k, tab.index)
		   end  
		)

--  import selection functions
require"lcselect"

-- index function for the global metatable.
-- converts variable in form A1 to luacalc.sheet[1][1]
-- references only - no assignments
-- this discourages assigning globals from a spreadsheet.
local function gmt(table, key)
   -- only interested in things that start
   -- with upper case letters, possibly 
   -- followed by numbers

   if string.find(key, "^%u+%d*$") then
      -- this is a cell reference
      -- print("Looking up global")
      -- print(key)
      local letter=""
      local number=""
      string.foreach(key, function(ind, char)
			     -- surely there is a more efficient check for capitals?
			     if string.byte(char) >= string.byte"A" and string.byte(char) <= string.byte("Z") then
				letter=letter..char
			     else
				number=number..char
			     end
			  end)
      local rowind = luacalc.string_to_index(letter)

      local ind = tonumber(number)
      local row = luacalc.sheet[ind]      
      if ind then 
	 -- need to convert functions to values
	 return luacalc.raw_value(row[rowind], ind, rowind)
      else
	 -- return the whole row
	 return luacalc.getselection{rowind, 1, rowind, #luacalc.sheet}
      end
   end
end

--
-- Sort works by sorting a table that stores (index, row) couples
-- where the index is the orignal luacalc.sheet index.
-- the comparator takes the original index and finds the values in the 
-- given column
-- 
--
function luacalc.sort(col)
	luacalc.dirty=true
	tosort = {}
	
	for i=1, #luacalc.sheet do
		table.insert(tosort, {index=i, row=luacalc.sheet[i]})
	end
	
	table.sort(tosort,
		function(a, b)
			local val_a = luacalc.sheet[a.index][col]
			val_a = luacalc.raw_value(val_a, a.index, col)
	
			local val_b = luacalc.sheet[b.index][col]
			val_b  = luacalc.raw_value(val_b , b.index, col)
	
			if val_a and val_b  then
				val_a = tonumber(val_a) or val_a
				val_b  = tonumber(val_b ) or val_b 
				if type(val_a)=="string" or type(val_b )=="string" then
					return tostring(val_a) < tostring(val_b )
				else
					return val_a < val_b 
				end
			else
				return val_a
			end
		end
		)
	local i = 1
	for _, v in next, tosort do
		luacalc.sheet[i] = v.row
		i = i+1
	end
end

-- undo the last command (not all actions
function luacalc.undo()
   return commandstack:undo()
end

-- redo the last command
function luacalc.redo()
   return commandstack:redo()
end

-- start/end copy are used so that undo can cope with copy/paste.
function luacalc.startpaste()
   assert(luacalc.inputmode=="normal")
   luacalc.inputmode="paste"
end

-- stop collecting commands for a paste.
function luacalc.endpaste()
   -- make a local copy of paste data
   local pastedata = {}
   for k, v in ipairs(luacalc.pastedata) do
      table.insert(pastedata, v)
   end
   commandstack:push{
      
      undo = function()
		-- undo each in fifo order
		for i=#pastedata, 1, -1 do
		   pastedata[i].undo()
		end
	     end,
      redo = function()
		-- redo each action
		for k, v in ipairs(pastedata) do
		   v.redo()
		end
	     end
   }
   luacalc.inputmode="normal"
   luacalc.pastedata={}
end

-- start recording a macro
-- overwrites any previously stored macro
function luacalc.startmacro(x, y)
	luacalc.x, luacalc.y = x, y
	luacalc.macro_running = true
	luacalc.macro = macrostack.new()
end

-- stops macro recording
function luacalc.stopmacro()
	luacalc.macro_running = false
end

--replay the currently loaded macro
function luacalc.replay()
	for _, v in luacalc.macro:ipairs() do
		v.func()
	--	print(v.text)
	end
	luacalc.macro_running = false
end

-- record a movement
function luacalc.move(x, y)
	luacalc.macro:append{
		text="luacalc._move("..x..", "..y..")",
		func = function() 
			luacalc.x = luacalc.x+x 
			luacalc.y = luacalc.y+y 
		end
	}
end

-- replace a movement (shouldn't be directly called)
function luacalc._move(x, y)
	luacalc.x = luacalc.x+x 
	luacalc.y = luacalc.y+y 
end

-- save the current macro
function luacalc.savemacro(fname)
	local f = io.open(fname, "w")
	for _, v in luacalc.macro:ipairs() do
		f:write(v.text)
		f:write("\n")
	end
	f:close()
end

-- load a previously saved macro (or a lua script)
function luacalc.loadmacro(fname)
	local text = io.open(fname):read("*all")
	--print(text)
	luacalc.macro =  macrostack.new()
	luacalc.macro:append{
		text=text,
		func = loadstring(text)
	}
	luacalc.macro_running=false
end

-- for the wxWidgets gui
-- cause I'm no good at using the lua C api
function get(i, j)
   local success, res = pcall(function() return luacalc.display_view[i][j] end)
   if not success then 
      return "#error: "..tostring(res) 
   else
      return tostring(res or '')
   end
end

function getformula(i, j)
   local val = luacalc.sheet[i][j]
   if type(val)=="table" then
      return val.text
   else
      return val
   end
end


-- set the value at (i, j) to val
-- can log the requests to allow for undo/redo
function set(i, j, val)
	local old = luacalc.sheet[i][j]
	luacalc.dirty=true
	local action = {
	undo = function()
			luacalc.sheet[i][j]=old
		end,
	redo= function()
			luacalc.sheet[i][j]=val
		end
	}
	
	if luacalc.inputmode == "normal" and #luacalc.pastedata == 0 then
		commandstack:push(action)
	elseif luacalc.inputmode == "normal" then
		error("Invalid state; there should be no paste data")
	else
		table.insert(luacalc.pastedata, action)
	end
	
	if luacalc.macro_running then
		luacalc.macro:append{
			text="set(luacalc.x, luacalc.y, "..(tonumber(val) or string.format("%q",string.trim(val)))..")",
			func=function()
				set(luacalc.x, luacalc.y, val)
			end
		}
	end	
	
	-- finally, do the actual assignment.
	luacalc.sheet[i][j]=val
end

function save(fname)
   luacalc.save_file(fname)
end

function open(fname)
   luacalc.load_file(fname)
end

-- input/show for non-graphical version:

input = input or function(v)
	io.write(v..">")
	return io.read()
end

show = show or print
	
-- disable this for debugging.
-- I'm sure this "shouldn't be done" - changing the behaviour
-- of all globals can't be good for modularity.
setmetatable(_G, {__index=gmt})


--function to test cpp binding.

function hello()
--   print("LuaCalc started")
	--show"welcome to luacalc"
	--show("***"..input("Enter your name").."**")
end


